/*
 *
 *  *
 *  *   Copyright 2020-2021 Luter.me
 *  *
 *  *   Licensed under the Apache License, Version 2.0 (the "License");
 *  *   you may not use this file except in compliance with the License.
 *  *   You may obtain a copy of the License at
 *  *
 *  *   http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  *   Unless required by applicable law or agreed to in writing, software
 *  *   distributed under the License is distributed on an "AS IS" BASIS,
 *  *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  *   See the License for the specific language governing permissions and
 *  *   limitations under the License.
 *  *
 *
 */

package com.luter.heimdall.core.cookie;

import com.luter.heimdall.core.config.ConfigManager;
import com.luter.heimdall.core.config.HeimdallProperties;
import com.luter.heimdall.core.config.options.SameSiteOptions;
import com.luter.heimdall.core.utils.ObjUtil;
import com.luter.heimdall.core.utils.Rfc6265Utils;
import com.luter.heimdall.core.utils.StrUtils;

import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

/**
 * Cookie 类
 * <p>
 * Cookie是由服务器发给客户端的特殊信息，而这些信息以文本文件的方式存放在客户端，
 * 然后客户端每次向服务器发送请求的时候都会带上这些特殊的信息，用于服务器记录客户端的状态。
 * 每个 Cookie 的大小一般不能超过4KB。
 * <p>
 * Cookie主要用于以下三个方面：
 * <p>
 * 1、会话状态管理（如用户登录状态、购物车、游戏分数或其它需要记录的信息）
 * <p>
 * 2、个性化设置（如用户自定义设置、主题等）
 * <p>
 * 3、浏览器行为跟踪（如跟踪分析用户行为等）
 *
 * @author luter
 */
public class SimpleCookie {
    /**
     * The constant GMT.
     */
    private static final ZoneId GMT = ZoneId.of("GMT");
    /**
     * The constant DATE_FORMATTER.
     */
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US).withZone(GMT);

    /**
     * The constant COOKIE_HEADER_NAME.
     */
    public static final String COOKIE_HEADER_NAME = "Set-Cookie";
    /**
     * The constant PATH_ATTRIBUTE_NAME.
     */
    protected static final String PATH_ATTRIBUTE_NAME = "Path";
    /**
     * The constant EXPIRES_ATTRIBUTE_NAME.
     */
    protected static final String EXPIRES_ATTRIBUTE_NAME = "Expires";
    /**
     * The constant MAX_AGE_ATTRIBUTE_NAME.
     */
    protected static final String MAX_AGE_ATTRIBUTE_NAME = "Max-Age";
    /**
     * The constant DOMAIN_ATTRIBUTE_NAME.
     */
    protected static final String DOMAIN_ATTRIBUTE_NAME = "Domain";
    /**
     * The constant SECURE_ATTRIBUTE_NAME.
     */
    protected static final String SECURE_ATTRIBUTE_NAME = "Secure";
    /**
     * The constant HTTP_ONLY_ATTRIBUTE_NAME.
     */
    protected static final String HTTP_ONLY_ATTRIBUTE_NAME = "HttpOnly";
    /**
     * The constant SAME_SITE_ATTRIBUTE_NAME.
     */
    protected static final String SAME_SITE_ATTRIBUTE_NAME = "SameSite";

    /**
     * Cookie 名称
     * <p>
     * 表示Cookie的名称，服务器就是通过name属性来获取某个Cookie值。
     */
    private String name;

    /**
     * Cookie 值
     * <p>
     * 表示Cookie 的值，大多数情况下服务器会把这个value当作一个key去缓存中查询保存的数据。
     */
    private String value;

    /**
     * 过期时长.单位：秒
     * <p>
     * Expires/Max-Age表示此cookie超时时间。
     * 若设置其值为一个时间，那么当到达此时间后，此cookie失效。
     * 不设置的话默认值是Session，意思是cookie会和session一起失效。
     * 当浏览器关闭(不是浏览器标签页，而是整个浏览器) 后，此cookie失效。
     */
    private Duration maxAge;
    /**
     * cookie所在的域
     * <p>
     * 表示可以访问此cookie的域名
     * <p>
     * 顶级域名只能设置或访问顶级域名的Cookie，
     * 二级及以下的域名只能访问或设置自身或者顶级域名的Cookie
     * <p>
     * 例如，如果设置 Domain=mozilla.org，则 Cookie 也包含在子域名中（如developer.mozilla.org）
     */
    private String domain;
    /**
     * cookie所在的目录
     * <p>
     * Path 标识指定了主机下的哪些路径可以接受 Cookie（该 URL 路径必须存在于请求 URL 中）。
     * 以字符 %x2F ("/") 作为路径分隔符，子路径也会被匹配。
     * <p>
     * 例如，设置 Path=/docs，则以下地址都会匹配：
     * <p>
     * /docs
     * <p>
     * /docs/Web/
     * <p>
     * /docs/Web/HTTP
     */
    private String path;
    /**
     * The Secure.
     * <p>
     * Secure表示是否只能通过https来传递此条cookie。
     * 该选项只是一个标记并且没有其它的值。
     */
    private boolean secure;
    /**
     * The Http only.
     * <p>
     * 表示cookie的httponly属性。若此属性为true，
     * <p>
     * 则只有在http请求头中会带有此cookie的信息，而不能通过document.cookie来访问此cookie。
     * <p>
     * 设计该特征意在提供一个安全措施来帮助阻止通过Javascript发起的跨站脚本攻击(XSS)窃取cookie的行为
     */
    private boolean httpOnly;
    /**
     * sameSite 属性用来限制第三方 Cookie，从而减少安全风险
     * <p>
     * SameSite Cookie 允许服务器要求某个 cookie 在跨站请求时不会被发送。
     * 从而可以阻止跨站请求伪造攻击（CSRF）
     */
    private SameSiteOptions sameSite;

    public SimpleCookie() {
        HeimdallProperties config = ConfigManager.getConfig();
        this.name = config.getToken().getName();
        this.maxAge = Duration.ofSeconds(config.getCookie().getMaxAge());
        this.sameSite = config.getCookie().getSameSite();
        this.path = config.getCookie().getPath();
        this.domain = config.getCookie().getDomain();
        this.httpOnly = config.getCookie().getHttpOnly();
        this.secure = config.getCookie().getSecure();
        Rfc6265Utils.validateCookieName(name);
        Rfc6265Utils.validateCookieValue(value);
        Rfc6265Utils.validateDomain(domain);
        Rfc6265Utils.validatePath(path);
    }

    public SimpleCookie(String value) {
        this();
        Rfc6265Utils.validateCookieValue(value);
        this.value = value;
    }

    public SimpleCookie(String name, String value) {
        this();
        Rfc6265Utils.validateCookieName(name);
        Rfc6265Utils.validateCookieValue(value);
        this.name = name;
        this.value = value;
    }

    public String getName() {
        return name;
    }

    public SimpleCookie setName(String name) {
        this.name = name;
        return this;
    }

    public String getValue() {
        return value;
    }

    public SimpleCookie setValue(String value) {
        this.value = value;
        return this;
    }

    public Duration getMaxAge() {
        return maxAge;
    }

    public SimpleCookie setMaxAge(Duration maxAge) {
        this.maxAge = maxAge;
        return this;
    }

    public String getDomain() {
        return domain;
    }

    public SimpleCookie setDomain(String domain) {
        this.domain = domain;
        return this;
    }

    public String getPath() {
        return path;
    }

    public SimpleCookie setPath(String path) {
        this.path = path;
        return this;
    }

    public boolean isSecure() {
        return secure;
    }

    public SimpleCookie setSecure(boolean secure) {
        this.secure = secure;
        return this;
    }

    public boolean isHttpOnly() {
        return httpOnly;
    }

    public SimpleCookie setHttpOnly(boolean httpOnly) {
        this.httpOnly = httpOnly;
        return this;
    }

    public SameSiteOptions getSameSite() {
        return sameSite;
    }

    public SimpleCookie setSameSite(SameSiteOptions sameSite) {
        this.sameSite = sameSite;
        return this;
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof SimpleCookie)) {
            return false;
        }
        SimpleCookie otherCookie = (SimpleCookie) other;
        return (getName().equalsIgnoreCase(otherCookie.getName()) &&
                ObjUtil.nullSafeEquals(this.path, otherCookie.getPath()) &&
                ObjUtil.nullSafeEquals(this.domain, otherCookie.getDomain()));
    }

    @Override
    public int hashCode() {
        int result = super.hashCode();
        result = 31 * result + ObjUtil.nullSafeHashCode(this.domain);
        result = 31 * result + ObjUtil.nullSafeHashCode(this.path);
        return result;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getName()).append('=').append(getValue());
        if (StrUtils.hasText(getPath())) {
            sb.append("; " + PATH_ATTRIBUTE_NAME + "=").append(getPath());
        }
        if (StrUtils.hasText(this.domain)) {
            sb.append("; " + DOMAIN_ATTRIBUTE_NAME + "=").append(this.domain);
        }
        if (!this.maxAge.isNegative()) {
            sb.append("; " + MAX_AGE_ATTRIBUTE_NAME + "=").append(this.maxAge.getSeconds());
            sb.append("; " + EXPIRES_ATTRIBUTE_NAME + "=");
            long millis = this.maxAge.getSeconds() > 0 ? System.currentTimeMillis() + this.maxAge.toMillis() : 0;
            sb.append(formatDate(millis));
        }
        if (this.secure) {
            sb.append("; " + SECURE_ATTRIBUTE_NAME + "");
        }
        if (this.httpOnly) {
            sb.append("; " + HTTP_ONLY_ATTRIBUTE_NAME + "");
        }
        if (StrUtils.hasText(this.sameSite.name())) {
            sb.append("; " + SAME_SITE_ATTRIBUTE_NAME + "=").append(this.sameSite);
        }
        return sb.toString();
    }

    /**
     * Format date string.
     *
     * @param date the date
     * @return the string
     */
    static String formatDate(long date) {
        Instant instant = Instant.ofEpochMilli(date);
        ZonedDateTime time = ZonedDateTime.ofInstant(instant, GMT);
        return DATE_FORMATTER.format(time);
    }

}